#!/usr/bin/env php
<?php declare(strict_types=1);

use Phan\AST\TolerantASTConverter\Shim;
use Phan\CodeBase;
use Phan\Language\Element\Clazz;
use Phan\Language\Element\Func;
use Phan\Language\Element\GlobalConstant;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedFunctionName;
use Phan\Language\FQSEN\FullyQualifiedGlobalConstantName;

require_once dirname(__DIR__) . '/src/requirements.php';
require_once dirname(__DIR__) . '/src/Phan/Bootstrap.php';

// If php-ast isn't loaded already, then load this file to generate equivalent
// class, constant, and function definitions.
Shim::load();

/**
 * Generates PHP stubs for extensions that can be used in the autoload_internal_extension_signatures `.phan/config.php` setting.
 * These are regular PHP files stubbing PHP modules, containing empty method implementations, etc.
 *
 * Configured stubs can be used in IDEs, analysis, etc. where extensions aren't installed or enabled (e.g. Xdebug)
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod, PhanPluginNoCommentOnPublicMethod
 * @phan-file-suppress PhanPluginRemoveDebugAny
 */
class StubsGenerator
{

    /**
     * @return never (does not return)
     */
    public static function printHelpAndExit(string $message = '', int $exit_code = 1) : void
    {
        if ($message !== '') {
            echo "$message\n";
        }
        global $argv;
        $prog_name = $argv[0];
        echo <<<EOT
Usage: $prog_name [--opts]
 -h, --help
  Print this help message

 -e, --extension extension_name
  Print stubs for only the given PECL or built in extension (e.g. 'ast', 'pcntl')

 -d, --doc-comment
  Include doc comments from the comment summaries bundled with Phan.

 -o, --output path/to/file
  Save the stubs to a file instead of printing them to stdout.

EOT;
        exit($exit_code);
    }

    /**
     * The main function of the `make_stubs` script.
     * See `make_stubs --help` for usage.
     */
    public static function main() : void
    {
        $options = getopt('he:o:d', ['help', 'extension:', 'output:', 'doc-comment']);
        if (isset($options['h']) || isset($options['help'])) {
            self::printHelpAndExit('', 0);
        }
        if (isset($options['d']) || isset($options['doc-comment'])) {
            $_ENV['PHAN_STUB_INCLUDE_DOC'] = '1';
        }

        $code_base = require(dirname(__DIR__) . '/src/codebase.php');

        $extension_name = $options['e'] ?? $options['extension'] ?? null;
        $output_path = $options['o'] ?? $options['output'] ?? null;

        if (is_string($extension_name)) {
            $output = self::generateStubsForExtension($extension_name, $code_base);
        } else {
            $output = self::generateAllStubs($code_base);
        }
        if (is_string($output_path)) {
            file_put_contents($output_path, $output);
            fwrite(STDERR, "Saved stubs for $extension_name to $output_path\n");
        } else {
            echo $output;
        }
    }

    private static function generateStubsForExtension(string $extension_name, CodeBase $code_base): string
    {
        $stub_collection = new StubCollection($code_base);
        $reflection_extension = new ReflectionExtension($extension_name);
        $extension_version = $reflection_extension->getVersion();

        self::recordClassStubsForExtension($stub_collection, $reflection_extension, $code_base);
        self::recordGlobalFunctionStubsForExtension($stub_collection, $reflection_extension, $code_base);
        self::recordGlobalConstantStubsForExtension($stub_collection, $reflection_extension, $code_base);

        $result = '';
        $result .= "<" . "?php\n";
        $result .= "// These stubs were generated by the phan stub generator.\n";
        $result .= "// @phan-stub-for-extension $extension_name@$extension_version\n";
        $result .= "\n";

        $result .= $stub_collection->toString();
        return $result;
    }

    private static function recordClassStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getClassNames() as $class_name) {
            try {
                $class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class_name);
            } catch (Exception $e) {
                // only possible if module info is wrong
                fwrite(STDERR, "Failed to parse fqsen of class $class_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasClassWithFQSEN($class_fqsen)) {
                fwrite(STDERR, "Failed to find class $class_fqsen\n");
                continue;
            }
            $stub_collection->addClazz($code_base->getClassByFQSEN($class_fqsen));
        }
    }

    private static function recordGlobalFunctionStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getFunctions() as $function_name => $unused_reflection_function) {
            try {
                $function_fqsen = FullyQualifiedFunctionName::fromFullyQualifiedString($function_name);
            } catch (Exception $e) {
                // only possible if module info is wrong
                fwrite(STDERR, "Failed to parse fqsen of function $function_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasFunctionWithFQSEN($function_fqsen)) {
                fwrite(STDERR, "Failed to find function $function_fqsen\n");
                continue;
            }
            $stub_collection->addFunc($code_base->getFunctionByFQSEN($function_fqsen));
        }
    }

    private static function recordGlobalConstantStubsForExtension(
        StubCollection $stub_collection,
        ReflectionExtension $reflection_extension,
        CodeBase $code_base
    ) : void {
        foreach ($reflection_extension->getConstants() as $constant_name => $_) {
            try {
                $const_fqsen = FullyQualifiedGlobalConstantName::fromFullyQualifiedString($constant_name);
            } catch (Exception $e) {
                // only possible if module info is invalid
                fwrite(STDERR, "Failed to parse fqsen of global constant $constant_name : {$e->getMessage()}\n");
                continue;
            }
            if (!$code_base->hasGlobalConstantWithFQSEN($const_fqsen)) {
                fwrite(STDERR, "Failed to find global constant $const_fqsen\n");
                continue;
            }
            $stub_collection->addGlobalConstant($code_base->getGlobalConstantByFQSEN($const_fqsen));
        }
    }

    public static function generateAllStubs(CodeBase $code_base): string
    {
        $code_base->eagerlyLoadAllSignatures();
        $stub_collection = new StubCollection($code_base);
        $class_map = $code_base->getInternalClassMap();

        $result = '';
        $result .= "<" . "?php\n";
        $result .= "// These stubs were generated by the phan stub generator.\n";

        foreach ($class_map as $class) {
            $stub_collection->addClazz($class);
        }

        $function_map = $code_base->getFunctionMap();
        foreach ($function_map as $function) {
            if ($function->getFQSEN()->isAlternate()) {
                continue;
            }
            $stub_collection->addFunc($function);
        }

        $const_map = $code_base->getGlobalConstantMap();

        foreach ($const_map as $const) {
            $stub_collection->addGlobalConstant($const);
        }
        $result .= $stub_collection->toString();
        return $result;
    }
}

StubsGenerator::main();

/**
 * A representation of the collection of stubs for elements of a PHP module(a.k.a. extension).
 */
class StubCollection
{
    /** @var CodeBase represents the known state of the code base we're extracting the stubs from. */
    private $code_base;

    public function __construct(CodeBase $code_base)
    {
        $this->code_base = $code_base;
    }

    /** @var string[][] a list of class stubs for a PHP module */
    public $class_stubs = [];
    /** @var string[][] a list of function stubs for a PHP module */
    public $function_stubs = [];
    /** @var string[][] a list of global constant stubs for a PHP module */
    public $global_constant_stubs = [];

    public function addClazz(Clazz $class) : void
    {
        [$namespace, $name] = $class->toStubInfo($this->code_base);
        $this->class_stubs[$namespace][(string)$class->getFQSEN()] = $name;
    }

    public function addGlobalConstant(GlobalConstant $global_constant) : void
    {
        [$namespace, $name] = $global_constant->toStubInfo();
        $this->global_constant_stubs[$namespace][(string)$global_constant->getFQSEN()] = $name;
    }

    public function addFunc(Func $function) : void
    {
        [$namespace, $name] = $function->toStubInfo();
        $this->function_stubs[$namespace][(string)$function->getFQSEN()] = $name;
    }

    /** @return string[][] */
    public function toCombinedStubs() : array
    {
        $result = [];
        foreach ($this->class_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        foreach ($this->function_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        foreach ($this->global_constant_stubs as $namespace => $stubs) {
            ksort($stubs, SORT_NATURAL);
            $result[$namespace] = array_merge($result[$namespace] ?? [], $stubs);
        }
        return $result;
    }

    /**
     * Returns the accumulated stubs converted to inline PHP code.
     */
    public function toString() : string
    {
        $parts = [];
        foreach ($this->toCombinedStubs() as $namespace => $stubs) {
            $concatenated_stubs_representation = implode('', $stubs);
            $namespace_repr = ($namespace === '' ? '' : "$namespace ");
            $parts[] = sprintf("namespace %s{\n%s}\n", $namespace_repr, $concatenated_stubs_representation);
        }
        return implode("\n", $parts);
    }
}
